using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using csammisrun.OscarLib.Utility;

namespace csammisrun.OscarLib
{
	#region Common SSI interface
	/// <summary>
	/// Defines an interface for all SSI items that exposes their unique ID
	/// </summary>
	internal interface IServerSideItem
	{
		/// <summary>
		/// The unique ID of the SSI item
		/// </summary>
		ushort ItemID { get; }
	}
	#endregion

	#region SSI classes
	/// <summary>
	/// Encapsulates an SSI item representing a group of contacts
	/// </summary>
	public class SSIGroup : IServerSideItem
	{
		/// <summary>
		/// The name of the group
		/// </summary>
		public string Name = "";
		/// <summary>
		/// The group's ID number
		/// </summary>
		public ushort ID = 0;
		/// <summary>
		/// The IDs of the <see cref="SSIBuddy"/> objects contained in this group
		/// </summary>
		public List<ushort> Children = new List<ushort>();
    /// <summary>
    /// The order of the group in the master list
    /// </summary>
    public int Order = 0;

		#region IServerSideItem Members
		/// <summary>
		/// The group ID of the group -- always zero
		/// </summary>
		public ushort ItemID
		{
			get
			{
				return 0;
			}
		}

		#endregion
	}

	/// <summary>
	/// Encapsulates an SSI item representing a single contact
	/// </summary>
	public class SSIBuddy : IServerSideItem
	{
    private ushort _groupid = 0;
    private string _name = "";
    private string _displayname = "";
    private string _email = "";
    private List<TLV> _unprocessed = new List<TLV>();

		/// <summary>
		/// The unique ID of the contact
		/// </summary>
		public readonly ushort ID;

		/// <summary>
		/// The ID of the contact's parent group
		/// </summary>
    public ushort GroupID
    {
      get { return _groupid; }
      set { _groupid = value; }
    }
		/// <summary>
		/// The actual name of the contact
		/// </summary>
    public string Name
    {
      get { return _name; }
      set { _name = value; }
    }
		/// <summary>
		/// The display name of the contact
		/// </summary>
    public string DisplayName
    {
      get { return _displayname; }
      set { _displayname = value; }
    }
		/// <summary>
		/// The locally assigned email address of this contact
		/// </summary>
    public string Email
    {
      get { return _email; }
      set { _email = value; }
    }
    /// <summary>
    /// Gets a list of TLVs sent by the server that went unprocessed by OscarLib
    /// </summary>
    /// <remarks>As of at least 11-2006, there are two TLVs that seem to be necessary in SSI item modifications, 0x006A and 0x006D.
    /// This list provides a handy place to keep track of them.</remarks>
    internal List<TLV> UnprocessedTLVs
    {
      get { return _unprocessed; }
    }
		/// <summary>
		/// The contact's SMS information
		/// </summary>
		public string SMS = "";
		/// <summary>
		/// A locally assigned comment about this contact
		/// </summary>
		public string Comment = "";
		/// <summary>
		/// Two bytes representing the different possible alert styles
		/// </summary>
		public ushort Alerts = 0;
		/// <summary>
		/// A sound file to play when this contact signs on
		/// </summary>
		public string SoundFile = "";
		/// <summary>
		/// Indicates whether the local client is awaiting authorization by the remote contact
		/// </summary>
		public bool AwaitingAuthorization = false;

		/// <summary>
		/// Creates a new SSIBuddy object
		/// </summary>
		/// <param name="id">The ItemID for the object</param>
		public SSIBuddy(ushort id)
		{
			this.ID = id;
		}

		#region IServerSideItem Members
		/// <summary>
		/// The unique ID of the contact
		/// </summary>
		public ushort ItemID
		{
			get
			{
				return ID;
			}
		}
		#endregion
	}

	/// <summary>
	/// Encapsulates an SSI item representing a single Permit item
	/// </summary>
	public class SSIPermit : IServerSideItem
	{
		/// <summary>
		/// The contact name associated with item
		/// </summary>
		public string Name;
		/// <summary>
		/// The unique ID of the item
		/// </summary>
		public readonly ushort ID;

		/// <summary>
		/// Creates a new SSIPermitDeny object
		/// </summary>
		/// <param name="id">The ItemID for the object</param>
		public SSIPermit(ushort id)
		{
			this.ID = id;
		}

		#region IServerSideItem Members
		/// <summary>
		/// The unique ID of the item
		/// </summary>
		public ushort ItemID
		{
			get
			{
				return ID;
			}
		}

		#endregion
	}

  /// <summary>
	/// Encapsulates an SSI item representing a single Deny item
	/// </summary>
	public class SSIDeny : IServerSideItem
	{
		/// <summary>
		/// The contact name associated with item
		/// </summary>
		public string Name;
		/// <summary>
		/// The unique ID of the item
		/// </summary>
		public readonly ushort ID;

		/// <summary>
		/// Creates a new SSIDeny object
		/// </summary>
		/// <param name="id">The ItemID for the object</param>
		public SSIDeny(ushort id)
		{
			this.ID = id;
		}

		#region IServerSideItem Members
		/// <summary>
		/// The unique ID of the item
		/// </summary>
		public ushort ItemID
		{
			get
			{
				return ID;
			}
		}

		#endregion
	}

	internal class SSIIcon : IServerSideItem
	{
		public byte[] Hash;
		public ushort ID = 0;

    public uint IconSize = 0;
    public ushort Checksum = 0;
    public uint Stamp = 0;

    /// <summary>
    /// Computes checksums and hashes for a buddy icon
    /// </summary>
    /// <param name="filename">The filename of the icon</param>
    /// <returns><c>true</c> if the server-side icon should be updated, <c>false</c> otherwise</returns>
    public bool ComputeItems(string filename)
    {
      bool retval = false;
      if (String.IsNullOrEmpty(filename))
        return retval;
      
      try
      {
        byte[] hash = null;
        FileStream fs = File.OpenRead(filename);
        DateTime epoch = new DateTime(1970, 1, 1, 8, 0, 0);

        this.Stamp = (uint)(File.GetLastWriteTime(filename) - epoch).TotalSeconds;
        this.IconSize = (uint)fs.Length;
        this.Checksum = ChecksumIcon(filename);

        hash = (new System.Security.Cryptography.MD5CryptoServiceProvider()).ComputeHash(fs);
        if (this.Hash == null)
        {
          this.Hash = hash;
        }

        for (int i = 0; i < this.Hash.Length; i++)
        {
          if (this.Hash[i] != hash[i])
            retval = true;
        }
      }
      catch
      {
        retval = false;
      }

      return retval;
    }

    private ushort ChecksumIcon(string filename)
    {
      uint retval = 0;
      int i = 0;

      StreamReader sr = null;
      try
      {
        sr = new StreamReader(filename);
        byte[] buffer = new byte[sr.BaseStream.Length];
        sr.BaseStream.Read(buffer, 0, buffer.Length);

        for (i = 0; i + 1 < buffer.Length; i += 2)
        {
          retval += (uint)((buffer[i + 1] << 8) + buffer[i]);
        }

        if (i < buffer.Length)
          retval += buffer[i];

        retval = ((retval & 0xffff0000) >> 16) + (retval & 0x0000ffff);
      }
      catch { }
      finally
      {
        if (sr != null)
          sr.Close();
      }

      return (ushort)retval;
    }

		#region IServerSideItem Members
		public ushort ItemID
		{
			get
			{
				return ID;
			}
		}
		#endregion
	}

  /// <summary>
  /// Encapsulates the permit/deny settings stored on the server
  /// </summary>
  internal class SSIPermitDenySetting : IServerSideItem
  {
    public PrivacySetting Privacy = PrivacySetting.AllowAllUsers;
    public uint AllowedUserClasses = 0xFFFFFFFF;

    public readonly ushort ID;

    public SSIPermitDenySetting(ushort id)
    {
      this.ID = id;
    }

    #region IServerSideItem Members

    public ushort ItemID
    {
      get { return this.ID; }
    }

    #endregion
  }

  #endregion

  /// <summary>
  /// Provides an interface for managing Server-Stored Information
  /// </summary>
  public class SSIManager
	{
		private const int SSI_TYPE_BUDDY = 0x0000;
		private const int SSI_BUDDY_ALIAS = 0x0131;
		private const int SSI_BUDDY_EMAIL = 0x0137;
		private const int SSI_BUDDY_SMS = 0x013A;
		private const int SSI_BUDDY_COMMENTS = 0x013C;
		private const int SSI_BUDDY_ALERTS = 0x013D;
		private const int SSI_BUDDY_SOUNDFILE = 0x013E;
		private const int SSI_BUDDY_AUTHORIZE = 0x0066;
    private const int SSI_BUDDY_DATA = 0x006D;

		private const int SSI_TYPE_GROUP = 0x0001;
		private const int SSI_GROUP_CHILDREN = 0x00C8;

		private const int SSI_TYPE_PERMIT = 0x0002;
		private const int SSI_TYPE_DENY = 0x0003;

		private const int SSI_TYPE_VISIBILITY = 0x0004;
    private const int SSI_VISIBILITY_PRIVACY = 0x00CA;
    private const int SSI_VISIBILITY_CLASSES = 0x00CB;

		private const int SSI_TYPE_IDLETIME = 0x0005;

		private const int SSI_TYPE_ICON = 0x0014;
		private const int SSI_ICON_HASH = 0x00D5;
		private const int SSI_ICON_PERSIST = 0x0131;

		private Session _parent = null;
		private ArrayList _items = null;
		private int _buddycount = 0;
		private int _groupcount = 0;
		private SSIIcon iconitem = null;

    private List<SSIGroup> _groups = new List<SSIGroup>();
    private List<SSIBuddy> _buddies = new List<SSIBuddy>();
    private List<SSIPermit> _permits = new List<SSIPermit>();
    private List<SSIDeny> _denies = new List<SSIDeny>();
    private SSIGroup _mastergroup = new SSIGroup();

		private uint _allowothers = 0;
		private bool _publicidletime = true;

		private int _requests = 0;

		/// <summary>
		/// Creates a new SSIManager
		/// </summary>
		public SSIManager(Session parent)
		{
			_parent = parent;
			_items = new ArrayList();
		}

		/// <summary>
		/// Adds an SSI item to the local list
		/// </summary>
		/// <param name="si">An <see cref="SSIItem"/> item</param>
		internal void AddSSIItem(SSIItem si)
		{
			switch(si.ItemType)
			{
				case SSIManager.SSI_TYPE_BUDDY:
					CreateBuddy(si);
					break;
				case SSIManager.SSI_TYPE_GROUP:
          if (si.GroupID != 0)
            CreateGroup(si, false);
          else
            CreateMasterGroup(si);
					break;
				case SSIManager.SSI_TYPE_PERMIT:
					CreatePermit(si);
					break;
				case SSIManager.SSI_TYPE_DENY:
					CreateDeny(si);
					break;
				case SSIManager.SSI_TYPE_VISIBILITY:
					ProcessVisibility(si);
					break;
				case SSIManager.SSI_TYPE_IDLETIME:
					ProcessIdleTime(si);
					break;
				case SSIManager.SSI_TYPE_ICON:
					ProcessIcon(si);
					break;
        default:
          Logging.WriteString("Got unrecognized SSI item of type " + si.ItemType);
          break;
			}
		}

    /// <summary>
    /// Updates an SSI item on the local list
    /// </summary>
    /// <param name="si">An <see cref="SSIItem"/> item</param>
    internal void UpdateSSIItem(SSIItem si)
    {
      switch (si.ItemType)
      {
        case SSIManager.SSI_TYPE_GROUP:
          CreateGroup(si, true);
          break;
      }
    }

		#region Group items
    Dictionary<ushort, int> order = new Dictionary<ushort, int>();

    internal void CreateMasterGroup(SSIItem item)
    {
      SSIGroup group = new SSIGroup();
      group.ID = item.GroupID;
      group.Name = item.Name;

      int numgroups = 0;

      //The master group includes the order of the groups
      foreach (TLV tlv in item.Tlvs)
      {
        if (tlv.TypeNumber == SSIManager.SSI_GROUP_CHILDREN)
        {
          numgroups = tlv.DataSize / 2;
          group.Children.Clear();
          for (int k = 0; k < numgroups; k++)
          {
            order.Add(Marshal.ByteArrayToUshort(tlv.Data, k * 2), k);
            group.Children.Add(Marshal.ByteArrayToUshort(tlv.Data, k * 2));
          }
        }
      }

      lock (this)
      {
        _items.Add(group);
        _mastergroup = group;
        _groupcount++;
      }

      //_parent.OnMasterGroupItemReceived(numgroups);
    }

		/// <summary>
		/// Creates a new <see cref="SSIGroup"/> from an SSI item sent by the server
		/// </summary>
		/// <param name="item">The received <see cref="SSIItem"/> object</param>
		internal void CreateGroup(SSIItem item, bool modify)
		{
      SSIGroup group = null;
      if (!modify)
      {
        group = new SSIGroup();
        group.ID = item.GroupID;

        lock (this)
        {
          _items.Add(group);
          _groups.Add(group);
          _groupcount++;
        }
      }
      else
      {
        group = GetGroupByID(item.GroupID);
      }
			
      group.Name = item.Name;
      if (order.ContainsKey(group.ID))
      {
        group.Order = order[group.ID];
      }
      else
      {
        group.Order = 0;
      }

			foreach(TLV tlv in item.Tlvs)
			{
				if(tlv.TypeNumber == SSIManager.SSI_GROUP_CHILDREN)
				{
					int j = tlv.DataSize / 2;
          group.Children.Clear();
					for(int k = 0; k < j; k++)
					{
						group.Children.Add(Marshal.ByteArrayToUshort(tlv.Data, k * 2));
					}
				}
			}
			
			_parent.OnGroupItemReceived(group);
		}

		/// <summary>
		/// Gets a list of all <see cref="SSIGroup"/> objects belonging to this client
		/// </summary>
		/// <returns>An array of <see cref="SSIGroup"/> objects</returns>
		public SSIGroup[] GetGroups()
		{
			int i = 0;
			SSIGroup[] retval = new SSIGroup[_groupcount];
      lock (this)
      {
			  foreach(object obj in _items)
			  {
				  if(obj is SSIGroup)
					  retval[i++] = (SSIGroup)obj;
			  }
      }
			return retval;
		}

		/// <summary>
		/// Gets a single <see cref="SSIGroup"/> object by its name
		/// </summary>
		/// <param name="name">The name of the group to return</param>
		/// <returns>
		/// A <see cref="SSIGroup"/> object, or <c>null</c> if there is no SSIGroup
		/// object with the given name
		/// </returns>
		public SSIGroup GetGroupByName(string name)
		{
      lock (this)
      {
        foreach (object obj in _items)
        {
          if (obj is SSIGroup && ((SSIGroup)obj).Name == name)
            return (SSIGroup)obj;
        }
      }
			return null;
		}

		/// <summary>
		/// Gets a single <see cref="SSIGroup"/> object by its ID number
		/// </summary>
		/// <param name="id">The ID number of the group to return</param>
		/// <returns>
		/// A <see cref="SSIGroup"/> object, or <c>null</c> if there is no SSIGroup
		/// object with the given ID number
		/// </returns>
		public SSIGroup GetGroupByID(ushort id)
		{
      lock (this)
      {
        foreach (object obj in _items)
        {
          if (obj is SSIGroup && ((SSIGroup)obj).ID == id)
            return (SSIGroup)obj;
        }
      }
			return null;
		}

		/// <summary>
		/// Adds a new group to the client's list
		/// </summary>
		/// <param name="groupname">The name of the new group</param>
    /// <param name="index">The index of the new group</param>
		/// <remarks>The group is initially empty</remarks>
		public SSIGroup AddGroup(string groupname, int index)
		{
      SSIGroup parent = GetGroupByID(0);
      int length = (parent.Children != null) ? parent.Children.Count : 0;
      if (index > length)
        throw new ArgumentOutOfRangeException();

			ushort newid = 0;
			lock(this)
			{
				newid = this.GetNextGroupID();
			}

			SSIItem newitem = new SSIItem();
			newitem.GroupID = newid;
			newitem.ItemID = 0;
			newitem.ItemType = SSIManager.SSI_TYPE_GROUP;
			newitem.Name = groupname;
			newitem.Tlvs = null;

			SSIGroup newgroup = new SSIGroup();
			newgroup.ID = newid;
			newgroup.Name = groupname;
			lock(this)
			{
				_items.Add(newgroup);
        _groups.Add(newgroup);
				_groupcount++;
			}

      SNAC13.SendEditStart(_parent);
			SNAC13.AddSSIItems(_parent, new SSIItem[] { newitem });
      AddChildToGroup(0, index, newid);
      SNAC13.SendEditComplete(_parent);

      return newgroup;
		}

		/// <summary>
		/// Renames an existing group
		/// </summary>
		/// <param name="groupID">The ID of the group to be renamed</param>
		/// <param name="newname">The new name of the group</param>
		public void RenameGroup(ushort groupID, string newname)
		{
			SSIGroup parent = GetGroupByID(groupID);
			if(parent == null)
			{
				// No such groupID
				return;
			}

      parent.Name = newname;

			SSIItem modification = new SSIItem();
			modification.GroupID = groupID;
			modification.ItemID = 0;
			modification.ItemType = SSIManager.SSI_TYPE_GROUP;
			modification.Name = newname;

			int i = 0, index = 0;
			int length = (parent.Children != null) ? parent.Children.Count : 0;

			byte[] newchildren = new byte[length * 2];
			for(; i < length; i++)
			{
				Marshal.InsertUshort(newchildren, parent.Children[i], ref index);
			}

			TLV[] tlvs = new TLV[1];
			tlvs[0] = TLVMarshal.MakeTLV(SSIManager.SSI_GROUP_CHILDREN, newchildren);
			modification.Tlvs = tlvs;

      SNAC13.SendEditStart(_parent);
			SNAC13.ModifySSIItems(_parent, new SSIItem[] { modification });
      SNAC13.SendEditComplete(_parent);
		}

		/// <summary>
		/// Adds a child item from a group
		/// </summary>
		/// <param name="groupID">The ID of the group to which to add a child</param>
    /// <param name="index">The index of the child in the group</param>
		/// <param name="childID">The ID of the child to add</param>
		protected void AddChildToGroup(ushort groupID, int index, ushort childID)
		{
			SSIGroup parent = GetGroupByID(groupID);
      int length = (parent.Children != null) ? parent.Children.Count : 0;
      if (index > length)
        throw new ArgumentOutOfRangeException();

			if(parent == null)
			{
				// No such groupID
				return;
			}
			SSIItem modification = new SSIItem();
			modification.GroupID = parent.ID;
			modification.ItemID = 0;
			modification.ItemType = SSIManager.SSI_TYPE_GROUP;
			modification.Name = parent.Name;

      // Add the new child to the SSIGroup's child list
      //ushort[] copychildren = new ushort[length + 1];
      //if (parent.Children != null)
      //{
      //  Array.Copy(parent.Children, 0, copychildren, 0, index);
      //  if(index < parent.Children.Length)
      //    Array.Copy(parent.Children, index, copychildren, index+1, parent.Children.Length - index);
      //}
      //copychildren[index] = childID;
      parent.Children.Add(childID);

      // Copy the children into a byte array
      int currindex = 0;
			byte[] newchildren = new byte[(length + 1) * 2];
			for(int i = 0; i < length+1; ++i)
			{
				Marshal.InsertUshort(newchildren, parent.Children[i], ref currindex);
			}

			TLV[] tlvs = new TLV[1];
			tlvs[0] = TLVMarshal.MakeTLV(SSIManager.SSI_GROUP_CHILDREN, newchildren);
			modification.Tlvs = tlvs;

			SNAC13.ModifySSIItems(_parent, new SSIItem[] { modification });
		}

    /// <summary>
    /// Shuffles children within a group
    /// </summary>
    /// <param name="groupID">The group to shuffle</param>
    /// <param name="index">The new index of the childID</param>
    /// <param name="childID">The ID to be moved</param>
    protected void ShuffleChildren(ushort groupID, int index, ushort childID)
    {
      SSIGroup parent = GetGroupByID(groupID);
      int length = (parent.Children != null) ? parent.Children.Count : 0;
      if (index >= length)
        throw new ArgumentOutOfRangeException();

      if (parent == null)
      {
        // No such groupID
        return;
      }
      
      SSIItem modification = new SSIItem();
      modification.GroupID = parent.ID;
      modification.ItemID = 0;
      modification.ItemType = SSIManager.SSI_TYPE_GROUP;
      modification.Name = parent.Name;

      int shiftindex = parent.Children.IndexOf(childID);
      ushort temp = parent.Children[shiftindex];
      parent.Children[shiftindex] = parent.Children[index];
      parent.Children[index] = temp;

      int currindex = 0;
      byte[] newchildren = new byte[length * 2];
      for (int i = 0; i < length; ++i)
      {
        Marshal.InsertUshort(newchildren, parent.Children[i], ref currindex);
      }

      TLV[] tlvs = new TLV[1];
      tlvs[0] = TLVMarshal.MakeTLV(SSIManager.SSI_GROUP_CHILDREN, newchildren);
      modification.Tlvs = tlvs;

      SNAC13.ModifySSIItems(_parent, new SSIItem[] { modification });
    }

		/// <summary>
		/// Removes a child item from a group
		/// </summary>
		/// <param name="groupID">The ID of the group from which to remove a child</param>
		/// <param name="childID">The ID of the child to remove</param>
		protected void RemoveChildFromGroup(ushort groupID, ushort childID)
		{
			SSIGroup parent = GetGroupByID(groupID);
			SSIItem modification = new SSIItem();
			modification.GroupID = parent.ID;
			modification.ItemID = 0;
			modification.ItemType = SSIManager.SSI_TYPE_GROUP;
			modification.Name = parent.Name;

			int index = 0;
			int length = (parent.Children != null) ? parent.Children.Count : 0;
			if(length == 0)
				return;

      if (!parent.Children.Remove(childID))
      {
        // Child ID didn't exist in the group
        return;
      }

      byte[] newchildren = new byte[parent.Children.Count];
			for(int i = 0; i < parent.Children.Count; i++)
			{
        Marshal.InsertUshort(newchildren, parent.Children[i], ref index);
			}

			TLV[] tlvs = new TLV[1];
			tlvs[0] = TLVMarshal.MakeTLV(SSIManager.SSI_GROUP_CHILDREN, newchildren);
			modification.Tlvs = tlvs;

			SNAC13.ModifySSIItems(_parent, new SSIItem[] { modification });
		}

    /// <summary>
    /// Moves a buddy
    /// </summary>
    /// <param name="buddy">The buddy to be moved</param>
    /// <param name="destination">The new parent group for the buddy</param>
    /// <param name="index">The new index in the destination group</param>
    public void MoveBuddy(SSIBuddy buddy, SSIGroup destination, int index)
    {
      if (buddy.GroupID != destination.ID)
      {
        this.RemoveBuddy(buddy);
        this.AddBuddy(buddy.Name, destination.ID, index,
          buddy.DisplayName, buddy.Email, buddy.SMS, buddy.Comment, buddy.SoundFile);
      }
      else
      {
        this.ShuffleChildren(destination.ID, index, buddy.ID);
      }
    }

    /// <summary>
    /// Moves a group within the contact list
    /// </summary>
    /// <param name="group">The group to move</param>
    /// <param name="index">The new index</param>
    public void MoveGroup(SSIGroup group, int index)
    {
      this.ShuffleChildren(0, index, group.ID);
    }

		/// <summary>
		/// Removes a group from the client's list
		/// </summary>
		/// <param name="group">The local <see cref="SSIGroup"/> object</param>
		/// <remarks>This method will remove all buddy items that are children of the group</remarks>
		public void RemoveGroup(SSIGroup group)
		{
      SNAC13.SendEditStart(this._parent);

			if(group.Children != null)
			{
				foreach(ushort child in group.Children)
				{
					this.RemoveBuddy(this.GetBuddyByID(child));
				}
			}

      lock (this)
      {
        _items.Remove(group);
        _groupcount--;
      }

			this.RemoveSSIItem(SSIManager.SSI_TYPE_GROUP, 0, group.ID, group.Name);

      SNAC13.SendEditComplete(this._parent);
		}
		#endregion

		#region Buddy items
		/// <summary>
		/// Creates a new <see cref="SSIBuddy"/> from an SSI item sent by the server
		/// </summary>
		/// <param name="item">The received <see cref="SSIItem"/> object</param>
		internal void CreateBuddy(SSIItem item)
		{
			SSIBuddy buddy = new SSIBuddy(item.ItemID);
			buddy.GroupID = item.GroupID;
			buddy.Name = item.Name;
			buddy.AwaitingAuthorization = false;

			foreach(TLV tlv in item.Tlvs)
			{
				switch(tlv.TypeNumber)
				{
					case SSIManager.SSI_BUDDY_ALIAS:
            buddy.DisplayName = Marshal.ByteArrayToString(tlv.Data, 0, tlv.DataSize, Marshal.ASCII);
						break;
					case SSIManager.SSI_BUDDY_EMAIL:
            buddy.Email = Marshal.ByteArrayToString(tlv.Data, 0, tlv.DataSize, Marshal.ASCII);
						break;
					case SSIManager.SSI_BUDDY_SMS:
            buddy.SMS = Marshal.ByteArrayToString(tlv.Data, 0, tlv.DataSize, Marshal.ASCII);
						break;
					case SSIManager.SSI_BUDDY_COMMENTS:
            buddy.Comment = Marshal.ByteArrayToString(tlv.Data, 0, tlv.DataSize, Marshal.ASCII);
						break;
					case SSIManager.SSI_BUDDY_ALERTS:
						buddy.Alerts = Marshal.ByteArrayToUshort(tlv.Data, 0);
						break;
					case SSIManager.SSI_BUDDY_SOUNDFILE:
            buddy.SoundFile = Marshal.ByteArrayToString(tlv.Data, 0, tlv.DataSize, Marshal.ASCII);
						break;
					case SSIManager.SSI_BUDDY_AUTHORIZE:
						buddy.AwaitingAuthorization = true;
						break;
          default:
            // Uh-huh...
            buddy.UnprocessedTLVs.Add(tlv);
            break;
				}
			}

      lock (this)
      {
        _items.Add(buddy);
        _buddies.Add(buddy);
        _buddycount++;
      }

			// Call event in Session
			_parent.OnBuddyItemReceived(buddy);
		}

		/// <summary>
		/// Gets a list of all <see cref="SSIBuddy"/> objects belonging to this client
		/// </summary>
		/// <returns>An array of <see cref="SSIBuddy"/> objects</returns>
		public SSIBuddy[] GetBuddies()
		{
			int i = 0;
			SSIBuddy[] retval = new SSIBuddy[_buddycount];
      lock (this)
      {
        foreach (object obj in _items)
        {
          if (obj is SSIBuddy)
            retval[i++] = obj as SSIBuddy;
        }
      }
			return retval;
		}

		/// <summary>
		/// Gets a list of <see cref="SSIBuddy"/> objects by a group ID number
		/// </summary>
		/// <param name="groupid">The group ID number of the buddies to return</param>
		/// <returns>An array of <see cref="SSIBuddy"/> objects</returns>
		public SSIBuddy[] GetBuddiesByGroupID(ushort groupid)
		{
			ArrayList retlist = new ArrayList();
      lock (this)
      {
        foreach (object obj in _items)
        {
          if (obj is SSIBuddy && ((SSIBuddy)obj).GroupID == groupid)
          {
            retlist.Add(obj);
          }
        }
      }

			SSIBuddy[] retval = new SSIBuddy[retlist.Count];
			for(int i = 0; i < retval.Length; i++)
			{
				retval[i] = (SSIBuddy)retlist[i];
			}
			return retval;
		}

		/// <summary>
		/// Gets a single buddy by its ID number
		/// </summary>
		/// <param name="buddyid">The ID number of the buddy to return</param>
		/// <returns>An <see cref="SSIBuddy"/> object, or <c>null</c>
		/// if the buddy ID does not exist</returns>
		public SSIBuddy GetBuddyByID(ushort buddyid)
		{
      lock (this)
      {
        foreach (object obj in _items)
        {
          if (obj is SSIBuddy && ((SSIBuddy)obj).ID == buddyid)
            return (SSIBuddy)obj;
        }
      }
			return null;
		}

		/// <summary>
		/// Adds a new buddy to the client's list
		/// </summary>
		/// <param name="name">The buddy's screen name</param>
		/// <param name="parentID">The ID of the group to which the buddy should be added</param>
    /// <param name="index">The index of the buddy in the group</param>
		/// <param name="alias">The alias of the buddy</param>
		/// <param name="email">The buddy's email address</param>
		/// <param name="SMS">The buddy's SMS contact number</param>
		/// <param name="comment">A comment about this buddy</param>
		/// <param name="soundfile">The path of a local soundfile associated with this buddy</param>
		/// <returns>The ID number of the newly created buddy</returns>
		/// <remarks>
		/// SSI modification requires acknowledgement from the server. The newly created SSIBuddy
		/// item will be returned to the client upon successful addition
		/// </remarks>
		public SSIBuddy AddBuddy(string name, ushort parentID, int index, string alias,
			string email, string SMS, string comment, string soundfile)
		{
			return AddModifyBuddy(name, parentID, index, alias, email, SMS, comment, soundfile, null);
		}

		/// <summary>
		/// Modifies a buddy on the client's list
		/// </summary>
		/// <param name="changebuddy">
		/// The local <see cref="SSIBuddy"/> object, with the required
		/// fields changed
		/// </param>
		/// <remarks>
		/// SSI modification requires acknowledgement from the server. The newly modified SSIBuddy
		/// item will be returned to the client upon successful addition
		/// </remarks>
		public void ModifyBuddy(SSIBuddy changebuddy)
		{
			AddModifyBuddy("", 0xFFFF, -1, "", "", "", "", "", changebuddy);
		}

		/// <summary>
		/// Removes a buddy from the client's list
		/// </summary>
		/// <param name="buddy">The local <see cref="SSIBuddy"/> object</param>
		public void RemoveBuddy(SSIBuddy buddy)
		{
      lock (this)
      {
        _items.Remove(buddy);
        _buddies.Remove(buddy);
        _buddycount--;
      }
			this.RemoveSSIItem(SSIManager.SSI_TYPE_BUDDY, buddy.ID, buddy.GroupID, buddy.Name);
			this.RemoveChildFromGroup(buddy.GroupID, buddy.ID);
		}

		/// <summary>
		/// Contains SSI item creation and sending routines for adding or modifying SSI buddy items
		/// </summary>
		/// <param name="name">The buddy's screen name</param>
		/// <param name="parentID">The ID of the group to which the buddy should be added</param>
    /// <param name="index">The new index of the buddy in the group</param>
		/// <param name="alias">The alias of the buddy</param>
		/// <param name="email">The buddy's email address</param>
		/// <param name="SMS">The buddy's SMS contact number</param>
		/// <param name="comment">A comment about this buddy</param>
		/// <param name="soundfile">The path of a local soundfile associated with this buddy</param>
		/// <param name="original">The original <see cref="SSIBuddy"/> object if this is a modification
		/// operation, or <c>null</c> if this is a creation operation</param>
		/// <returns></returns>
		protected internal SSIBuddy AddModifyBuddy(string name, ushort parentID, int index, string alias,
			string email, string SMS, string comment, string soundfile, SSIBuddy original)
		{
			ushort newid = 0;
      if (original != null)
      {
        newid = original.ID;
        name = original.Name;
        parentID = original.GroupID;
        alias = original.DisplayName;
        email = original.Email;
        soundfile = original.SoundFile;
      }
      else
      {
        lock (this)
        {
          newid = this.GetNextItemID();
        }
      }

			SSIItem newitem = new SSIItem();
			newitem.ItemID = newid;
			newitem.GroupID = parentID;
			newitem.ItemType = SSIManager.SSI_TYPE_BUDDY;
			newitem.Name = name;

			int tlvnum = 0;
      if (!String.IsNullOrEmpty(alias))
				tlvnum++;
			if(!String.IsNullOrEmpty(email))
				tlvnum++;
			if(!String.IsNullOrEmpty(SMS))
				tlvnum++;
			if(!String.IsNullOrEmpty(comment))
				tlvnum++;
			if(!String.IsNullOrEmpty(soundfile))
				tlvnum++;
      if (original != null)
      {
        tlvnum += original.UnprocessedTLVs.Count;
      }

			TLV[] tlvs = new TLV[tlvnum];
			tlvnum = 0;
			if(!String.IsNullOrEmpty(alias))
				tlvs[tlvnum++] = TLVMarshal.MakeTLV(
          SSIManager.SSI_BUDDY_ALIAS, Marshal.StringToByteArray(alias, Marshal.ASCII));
			if(!String.IsNullOrEmpty(email))
				tlvs[tlvnum++] = TLVMarshal.MakeTLV(
          SSIManager.SSI_BUDDY_EMAIL, Marshal.StringToByteArray(email, Marshal.ASCII));
			if(!String.IsNullOrEmpty(SMS))
				tlvs[tlvnum++] = TLVMarshal.MakeTLV(
          SSIManager.SSI_BUDDY_SMS, Marshal.StringToByteArray(SMS, Marshal.ASCII));
			if(!String.IsNullOrEmpty(comment))
				tlvs[tlvnum++] = TLVMarshal.MakeTLV(
          SSIManager.SSI_BUDDY_COMMENTS, Marshal.StringToByteArray(comment, Marshal.ASCII));
			if(!String.IsNullOrEmpty(soundfile))
				tlvs[tlvnum++] = TLVMarshal.MakeTLV(
          SSIManager.SSI_BUDDY_SOUNDFILE, Marshal.StringToByteArray(soundfile, Marshal.ASCII));
      if (original != null)
      {
        foreach (TLV tlv in original.UnprocessedTLVs)
        {
          tlvs[tlvnum++] = tlv;
        }
      }

			newitem.Tlvs = tlvs;

			SSIBuddy store = new SSIBuddy(newid);
			store.Name = name;
			store.GroupID = parentID;
			store.DisplayName = alias;
			store.Email = email;
			store.SMS = SMS;
			store.Comment = comment;
			store.SoundFile = soundfile;

      if (original == null)
      {
        _items.Add(store);
        _buddies.Add(store);
        _buddycount++;
        SNAC13.AddSSIItems(_parent, new SSIItem[] { newitem });
        this.AddChildToGroup(parentID, index, newid);
      }
			else
				SNAC13.ModifySSIItems(_parent, new SSIItem[] { newitem });

			return store;
		}
		#endregion

		#region Permit and deny items
		/// <summary>
		/// Creates a new <see cref="SSIPermitDeny"/> object from an SSI permit item sent by the server
		/// </summary>
		/// <param name="item">The received <see cref="SSIItem"/> object</param>
		internal void CreatePermit(SSIItem item)
		{
			SSIPermit permit = new SSIPermit(item.ItemID);
			permit.Name = item.Name;
      lock (this)
      {
        _items.Add(permit);
        _permits.Add(permit);
      }
		}

    /// <summary>
    /// Adds a new permit item to the client's list
    /// </summary>
    /// <param name="screenname">The screenname to add to the permit list</param>
    public void AddPermitItem(string screenname)
    {
      ushort newid = 0;
      lock (this)
      {
        newid = this.GetNextItemID();
      }

      SSIItem newitem = new SSIItem();
      newitem.GroupID = 0;
      newitem.ItemID = newid;
      newitem.ItemType = SSIManager.SSI_TYPE_PERMIT;
      newitem.Name = screenname;
      newitem.Tlvs = null;

      SSIPermit newpermit = new SSIPermit(newid);
      newpermit.Name = screenname;
      lock (this)
      {
        _items.Add(newpermit);
        _permits.Add(newpermit);
      }

      SNAC13.AddSSIItems(_parent, new SSIItem[] { newitem });
    }

    /// <summary>
    /// Removes an item from the client's permit list
    /// </summary>
    /// <param name="removeitem">The local <see cref="SSIPermitDeny"/> object to remove</param>
    public void RemovePermitItem(SSIPermit removeitem)
    {
      lock (this)
      {
        _items.Remove(removeitem);
        _permits.Remove(removeitem);
      }
      this.RemoveSSIItem(SSIManager.SSI_TYPE_PERMIT, removeitem.ID, 0, removeitem.Name);
    }

		/// <summary>
		/// Creates a new <see cref="SSIPermitDeny"/> from an SSI deny item sent by the server
		/// </summary>
		/// <param name="item">The received <see cref="SSIItem"/> object</param>
		internal void CreateDeny(SSIItem item)
		{
			SSIDeny deny = new SSIDeny(item.ItemID);
			deny.Name = item.Name;
      lock (this)
      {
        _items.Add(deny);
        _denies.Add(deny);
      }
		}

    /// <summary>
    /// Adds a new deny item to the client's list
    /// </summary>
    /// <param name="screenname">The screenname to add to the deny list</param>
    public void AddDenyItem(string screenname)
    {
      ushort newid = 0;
      lock (this)
      {
        newid = this.GetNextItemID();
      }

      SSIItem newitem = new SSIItem();
      newitem.GroupID = 0;
      newitem.ItemID = newid;
      newitem.ItemType = SSIManager.SSI_TYPE_DENY;
      newitem.Name = screenname;
      newitem.Tlvs = null;

      SSIDeny newdeny = new SSIDeny(newid);
      newdeny.Name = screenname;
      lock (this)
      {
        _items.Add(newdeny);
        _denies.Add(newdeny);
      }

      SNAC13.AddSSIItems(_parent, new SSIItem[] { newitem });
    }

    /// <summary>
    /// Removes an item from the client's deny list
    /// </summary>
    /// <param name="removeitem">The local <see cref="SSIPermitDeny"/> object to remove</param>
    public void RemoveDenyItem(SSIDeny removeitem)
    {
      lock (this)
      {
        _items.Remove(removeitem);
        _denies.Remove(removeitem);
      }
      this.RemoveSSIItem(SSIManager.SSI_TYPE_DENY, removeitem.ID, 0, removeitem.Name);
    }
		#endregion

		#region Icon settings
    /// <summary>
    /// Processes a server-stored icon hash
    /// </summary>
    /// <param name="item">An <see cref="SSIItem"/> sent by the server</param>
		internal void ProcessIcon(SSIItem item)
		{
			if(iconitem == null)
			{
				iconitem = new SSIIcon();
			}
			iconitem.ID = item.ItemID;

			foreach(TLV tlv in item.Tlvs)
			{
				switch(tlv.TypeNumber)
				{
					case SSI_ICON_HASH:
						iconitem.Hash = new byte[16];
						Marshal.CopyArray(tlv.Data, iconitem.Hash, 2);
						break;
				}
			}
			lock(this)
			{
				_items.Add(iconitem);
			}

      if (_parent.UploadIconFilename != "")
      {
        // The user has set a buddy icon, see if it is different
        // than the one that is on the server already, and if so, fix that
        if (iconitem.ComputeItems(_parent.UploadIconFilename))
          UpdateIconHash();
      }
		}

		/// <summary>
		/// Updates the server-stored icon information
		/// </summary>
		protected internal void UpdateIconHash()
		{
			StreamReader sr = null;
			byte[] hash = null;
			bool modify = true;
			System.Security.Cryptography.MD5CryptoServiceProvider md5 = 
				new System.Security.Cryptography.MD5CryptoServiceProvider();

			try
			{
				sr = new StreamReader(_parent.UploadIconFilename);
        byte[] filecontents = new byte[sr.BaseStream.Length];
        sr.BaseStream.Read(filecontents, 0, filecontents.Length);
        hash = md5.ComputeHash(filecontents);
        filecontents = null;
			}
			catch(Exception)
			{
				_parent.OnBuddyIconUploadFailed("Couldn't read " + _parent.UploadIconFilename);
			}
			finally
			{
				if(sr != null)
					sr.Close();
			}

			if(iconitem == null)
			{
				iconitem = new SSIIcon();
				lock(this)
				{
          iconitem.ID = this.GetNextItemID(); // 0x51F4;
					_items.Add(iconitem);
				}
				modify = false;
			}
			iconitem.Hash = hash;

			SSIItem item = new SSIItem();
			item.Name = "1";
			item.GroupID = 0;
			item.ItemID = iconitem.ID;
			item.ItemType = SSI_TYPE_ICON;

			byte[] data = new byte[iconitem.Hash.Length + 2];
			data[0] = 0x00;
			data[1] = 0x10;
			Marshal.CopyArray(iconitem.Hash, data, 0, 2);

			TLV[] tlvs = new TLV[2];
			tlvs[0] = TLVMarshal.MakeTLV(SSI_ICON_HASH, data);
			tlvs[1] = TLVMarshal.MakeTLV(SSI_ICON_PERSIST, null);

			item.Tlvs = tlvs;

			if(modify)
				SNAC13.ModifySSIItems(_parent, new SSIItem[] { item });
			else
				SNAC13.AddSSIItems(_parent, new SSIItem[] { item });
		}

		/// <summary>
		/// Removes the server-stored icon information item
		/// </summary>
		public void RemoveIconHash()
		{
			if(iconitem == null)
				return;

			SSIItem item = new SSIItem();
			item.ItemType = SSIManager.SSI_TYPE_ICON;
			item.ItemID = iconitem.ID;
			item.Name = "1";
			item.GroupID = 0;
			lock(this)
			{
				_items.Remove(iconitem);
				iconitem = null;
			}
			SNAC13.RemoveSSIItems(_parent, new SSIItem[] { item });
		}
		#endregion

		#region Visibility settings
		/// <summary>
		/// Processes server-stored visibility settings
		/// </summary>
		/// <param name="item"></param>
		internal void ProcessVisibility(SSIItem item)
		{
      SSIPermitDenySetting pds = new SSIPermitDenySetting(item.ItemID);

			foreach(TLV tlv in item.Tlvs)
			{
				switch(tlv.TypeNumber)
				{
					case SSI_VISIBILITY_PRIVACY:
						pds.Privacy = (PrivacySetting)tlv.Data[0];
						break;
					case SSI_VISIBILITY_CLASSES:
						pds.AllowedUserClasses = Marshal.ByteArrayToUint(tlv.Data, 0);
						break;
					case 0x00CC:
						_allowothers = Marshal.ByteArrayToUint(tlv.Data, 0);
						break;
				}
			}

      lock (this)
      {
        _items.Add(pds);
      }
		}

    internal SSIPermitDenySetting GetPDSetting()
    {
      lock (this)
      {
        foreach (object obj in _items)
        {
          if (obj is SSIPermitDenySetting)
            return (SSIPermitDenySetting)obj;
        }
      }
      return null;
    }

    internal void SetPDSetting(SSIPermitDenySetting pds, bool newitem)
    {
      SSIItem item = new SSIItem();
      item.GroupID = 0;
      item.ItemID = pds.ItemID;
      item.Name = "";
      item.ItemType = SSIManager.SSI_TYPE_VISIBILITY;

      TLV[] tlvs = new TLV[2];
      tlvs[0] = TLVMarshal.MakeTLV(SSI_VISIBILITY_PRIVACY,
        Marshal.UShortToByteArray((ushort)pds.Privacy));
      tlvs[1] = TLVMarshal.MakeTLV(SSI_VISIBILITY_CLASSES,
        Marshal.UIntToByteArray(pds.AllowedUserClasses));

      item.Tlvs = tlvs;

      if (newitem)
        SNAC13.AddSSIItems(_parent, new SSIItem[] { item });
      else
        SNAC13.ModifySSIItems(_parent, new SSIItem[] { item });
    }

    /// <summary>
    /// Gets or sets the client's <see cref="PrivacySetting"/>
    /// </summary>
		public PrivacySetting Privacy
		{
      get
      {
        SSIPermitDenySetting retval = GetPDSetting();

        if (retval != null)
          return retval.Privacy;
        else
          return PrivacySetting.AllowAllUsers;
      }
			set
			{
        SSIPermitDenySetting pd = GetPDSetting();
        bool upload = false;
        bool newitem = false;

        if (pd == null)
        {
          // Would this ever happen? I think all accounts have a PDS by
          // default. I might be wrong though, it's happened before.
          lock (this)
          {
            pd = new SSIPermitDenySetting(this.GetNextItemID());
            _items.Add(pd);
          }
          upload = true;
          newitem = true;
        }

        if (pd.Privacy != value)
        {
          pd.Privacy = value;
          upload = true;
        }

        if (upload)
        {
          SetPDSetting(pd, newitem);
        }
			}
		}

    /// <summary>
    /// Gets or sets the user classes that are allowed to contact the client
    /// </summary>
		public uint AllowedUserClasses
		{
			get
      {
        SSIPermitDenySetting retval = GetPDSetting();
        if (retval != null)
          return retval.AllowedUserClasses;
        else
          return 0xFFFFFFFF;
      }
			set
			{
        SSIPermitDenySetting pd = GetPDSetting();
        bool upload = false;
        bool newitem = false;

        if (pd == null)
        {
          lock (this)
          {
            pd = new SSIPermitDenySetting(this.GetNextItemID());
            _items.Add(pd);
          }
          upload = true;
          newitem = true;
        }

        if (pd.AllowedUserClasses != value)
        {
          pd.AllowedUserClasses = value;
          upload = true;
        }

        if (upload)
          SetPDSetting(pd, newitem);
			}
		}
		#endregion

		#region Idle time settings
		/// <summary>
		/// Processes server-stored idle time settings
		/// </summary>
		/// <param name="item"></param>
		internal void ProcessIdleTime(SSIItem item)
		{
			foreach(TLV tlv in item.Tlvs)
			{
				if(tlv.TypeNumber == 0x00C9)
				{
					_publicidletime =
						(Marshal.ByteArrayToUint(tlv.Data, 0) == 0x0000046F);
				}
			}
		}
		#endregion

    #region Utilities and properties
    /// <summary>
		/// Removes an SSI item from the server-stored list
		/// </summary>
		/// <param name="type">The type of the item to remove</param>
		/// <param name="ID">The item's ID number</param>
		/// <param name="groupID">The item's group ID number</param>
		/// <param name="name">The name of the group</param>
		protected internal void RemoveSSIItem(ushort type, ushort ID, ushort groupID, string name)
		{
			SSIItem removal = new SSIItem();
			removal.ItemType = type;
			removal.ItemID = ID;
			removal.GroupID = groupID;
			removal.Name = name;
			removal.Tlvs = null;

			SNAC13.RemoveSSIItems(_parent, new SSIItem[] { removal });
		}

		private ushort GetNextItemID()
		{
			ushort retval = 0;
			bool foundid = false;
			while(!foundid)
			{
				foundid = true;
				foreach(IServerSideItem ssi in _items)
				{
					if(ssi.ItemID == retval)
					{
						retval++;
						foundid = false;
						break;
					}
				}
			}
			return retval;
		}

		private ushort GetNextGroupID()
		{
			ushort retval = 1;	// 0 is reserved for the non-stored master group
			bool foundid = false;
			while(!foundid)
			{
				foundid = true;
				foreach(object obj in _items)
				{
					if(obj is SSIGroup && ((SSIGroup)obj).ID == retval)
					{
						retval++;
						foundid = false;
						break;
					}
				}
			}
			return retval;
		}

		/// <summary>
		/// Gets or sets a value indicating whether the SSI manager is waiting for the
		/// server to acknowledge a modification
		/// </summary>
		protected internal int OutstandingRequests
		{
			get
			{
				lock(this)
				{
					return _requests;
				}
			}
			set
			{
				lock(this)
				{
					_requests = value;
				}
			}
    }

    internal SSIIcon IconItem
    {
      get { return iconitem; }
      set { iconitem = value; }
    }

    /// <summary>
    /// Gets a list of all current SSIBuddy items
    /// </summary>
    public List<SSIBuddy> Buddies
    {
      get { return _buddies; }
    }

    /// <summary>
    /// Gets a list of all current SSIGroup items
    /// </summary>
    public List<SSIGroup> Groups
    {
      get { return _groups; }
    }

    /// <summary>
    /// Gets a list of all current SSIPermit items
    /// </summary>
    public List<SSIPermit> Permits
    {
      get { return _permits; }
    }

    /// <summary>
    /// Gets a list of all current SSIDeny items
    /// </summary>
    public List<SSIDeny> Denies
    {
      get { return _denies; }
    }

    /// <summary>
    /// Gets the master SSIGroup for this client
    /// </summary>
    public SSIGroup MasterGroup
    {
      get { return _mastergroup; }
    }
    #endregion

  }
}
